-
-
Notifications
You must be signed in to change notification settings - Fork 350
Webcodecs: Handle limited range pixel values in NV12 data. #1606
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: webcodecs
Are you sure you want to change the base?
Conversation
NV12 data. "Full range" means that the pixel data ranges from 0 to 255. "Limited range" means that it's using a more constrained range, such as 16 to 235. Because the range is being clamped, it prevents images decoded with the VideoDecoder API from using the full range of color. Notably, this caused fully transparent backgrounds to be light gray due to the bottom end of the range starting at 16 instead of 0. Also, explicilty handle the mono image channel used to encode transparency information in HEIC images. Lastly, add the copyright header to decoder_webcodecs.h.
There are two different modes in video that rarely existed in images, but now that video and image share formats, they have become relevant. What's often called full-range, PC-mode, or JPEG-mode is from 0-255, while limited-range, TV-mode, or video-mode is from 16-235 (16-240 chroma), with those numbers scaled up for 10+ bit. The latter is by far the most common for any video you'll encounter, the former for images. Libheif supports both via the full_range_flag in nclx, and will flag an image as being in limited or full range -- the decoder should handle that. (Which decoders have been getting much better about actually doing.) I think you can override it to get the right color conversion even when decoding. That saves the hit of an extra color conversion step plus additional round-off errors from the scaling. |
I tried to use that information to update the approach, but wasn't able to get something fully working. Here's what I tried: Create the nclx color profile:
Implemented conversion functions to convert from the values returned by the browser's decoded VideoFrame:
It would also be possible to grab this data from the VUI section of the SPS data, but the SPS parsing function we've got in this file doesn't have code to parse the VUI info. I printed some debug output to make sure this was all getting set properly, and I saw primaries = smpte170m (6), transfer = bt709 (1), matrix = smpte170m (6), and full range = 0. All these values seem correct to me, so I'm not quite sure why it doesn't work. Is there anything special you have to do when using the image data, such as over in post.js? |
I've investigated this further and discovered two things that prevent setting of the nclx profile from working as expected.
After this investigation I can see that it doesn't make sense to do the limited range color conversion in the decoder plugin. So I will go ahead and update this PR just to properly set the nclx profile info on the decoded image. However, I still think both things I raised above should be addressed to make color conversion & preserve the alpha channel properly for data returned from the webcodecs plugin. If there's any direction on that and whether you'd like me to take either thing on let me know. Thanks! |
the decoded tiles. Remove custom limited range color remapping that was being done in the plugin.
@scriby Could you please attach the image with the limited-range alpha channel? Do you know which software created this? The HEIF standard does not tell anything about the range of the alpha data. I can understand that images originally converted from JPEG use the limited range also in the HEIF file for the color channels, but I assumed that the alpha plane will always be full-range. I also see no way how a decoder could decide which range is correct. The |
I created this file by taking a picture on an iphone, long pressing on a subject in the photo, copying it, and then pasting into iMessage and sending the message. This image decodes properly with a transparent background when using libde265. So I'm not sure whether the original image is using limited range, or whether the browser decoding APIs are just producing limited range data anyway. Update: I tested several HEIC images and they all decode with |
Thanks for the example image. This image sets
For the alpha plane, there is no The question is still where the decoder gets the The best way to handle this seems to be to work around this browser bug by scaling it back to 0-255 in the decoder plugin. BTW: I see that there is a alpha-channel metadata box Instead of a full-range flag, there might be the values |
PS: I had a look into the VUIs of the H.265 bitstreams of your example image. The VUIs are correct. This matches the |
Heads up that this doesn't just affect the alpha channel. The actual color data is also using a limited range. IIRC the color data is using 16 - 240 and the alpha channel uses 16 - 235. So even the normal colors of browser decoded images are off (at least using my hardware, I haven't been able to test this across different hardware yet). I think there are 2 main ways to resolve it:
I've noticed that even though I'm setting nclx color profile info on the decoded image (similar to the decoder_libde265 plugin), it's not actually used by libheif. I think libheif may be defaulting to the info from the HEIF container's metadata. This would also require an update to the yuv2rgb code to also map the alpha channel, which you might not want in general. It could be added as a setting on the nclx color profile data though (default off).
As it is now, the plugin doesn't have enough information to know whether it should re-map or not. It would need to check for whether there is a mismatch between the full range flag that was returned by the decoder and what is specified on the colr box, or possibly the VUI info from SPS. The SPS parsing code in the plugin doesn't parse the VUI data, so it would need to be updated to read the full range flag out of VUI. Investigating into the bug a little more, it looks like the hardware decoder on macOS may always returns limited range colors for HEVC streams: mpv-player/mpv#6546 |
Do you know whether the browser decoder scales the output values or whether it just clips them to the limited range? |
Just by looking at it I think it looks scaled as opposed to clamped. I'll do some comparisons with the raw output, the scaled output, and what libde265 returns to make sure. |
I've got some interesting results from testing.
The overall result is that this PR improves colors for some images with the webcodec decoder when the decoder returns limited range colors (such as on my mac and perhaps every mac). Comparing the colr boxes between the image that works well (go board.heic.zip) and the one that doesn't work well (2521.heic), the difference is that 2521.heic has colour_type = nclx and "go board.heic" has Best guess is that when the overall colour_type is prof, the nclx color profile from the decoder will be used, whereas when there is already a nclx profile in the container, it will be used instead. As far as solving the issue, my sense is that libheif should use the nclx profile that the decoder sets (if present), and otherwise falls back to the one in the heif metadata. That would at least allow libheif to re-map the color data in this case. As for fixing up the alpha channel, the plugin itself could do that if libheif doesn't think it's the right call to scale the alpha channel in general |
libheif/libheif/image-items/image_item.cc Lines 857 to 860 in 0e043e6
Adding I can see in the comment that it was an intentional choice to to prefer the nclx profile provided in the HEIF metadata if both are present. I'm not sure if something else would be impacted by making this change. Given this info do you have any guidance for how you'd like to proceed? |
FWIW, this is required by ISO/IEC 14496-12:2022 Section 12.1.5.1:
ISO/IEC 23008-12 Section 6.5.5 just points back to 14496-12 for |
Thanks for that info @bradh. I'll try to explain why I think it doesn't apply (at least, directly) in this case. In the 2521.heic file, the colr box and SPS VUI data have matching nclx color profiles. So there's no mismatch in terms of input data and either could be used. The issue is that the decoder (in this case, device hardware) is for whatever reason changing the color profile when decoding. So now the "ground truth" about which color profile is being used has changed, and if we use the color profile in the HEIF metadata it will no longer be correct. Apparently this is the first decoder plugin which exhibits this sort of behavior, and the expectation up until now was that decoders would return color information in the same profile as the input source. I don't know whether it's a spec violation or whatnot for a decoder to change the color profile when decoding, but there's not much I can do about that and just need to figure out what to do with the output. |
I think we agree that the browser decoder illegally changes the decoded YCbCr values. It would not matter much if it just returned wrong VUI/nclx parameters, but as you said, it actually changes the decoded pixel values. Our real problem is that we do not understand yet what the decoder is actually doing and why it is doing that. Unfortunately, I didn't find the time yet to run this on my computers. But if you had a version online (e.g. https://strukturag.github.io/libheif/ with the new plugin), it should be easy to collect information on different browser/graphics card behavior. |
range. The browser VideoDecoder API may return limited range color data even when the input source is using full range. The webcodecs plugin corrects for this by inspecting whether the output is encoded in full range or not. Note that if the original source is using limited range that this scaling could cause libheif to scale the data again when converting to RGB. This isn't easy to avoid at this time b/c plugins don't know whether the input source is using limited range or not. While it is technically present in the VUI SPS data, it is rather complicated to parse this structure as a one-off.
I've pushed an update to the pull request to convert limited range to full range in the plugin if the result from the webcodecs API indicates it's using limited range colors. It works well in my local testing but I'll see if I can get something hosted so you can see it as well. |
Alright, I've got a hosted version of this PR up at https://scriby.github.io/libheif/. I just made one modification to print out a debugging line to make it easier to see what the browser API is returning. Check for something like this below the image:
|
I tried it out on a few different devices. Chrome on Mac, Android, and Windows all returned NV12 data with The only place I've seen Update: I did some more testing on Windows. I found a HEIC image online for which Chrome will decode with In Edge, both the image I found online and my iPhone photos decode as Neither Chrome nor Edge on my Windows machine will decode 2521.heic at all (image with alpha channel). |
NV12 data coming from the browser's VideoDecoder API may be using a different range of pixel values (instead of 0-255). The pixel ranges need to be re-mapped to use the full range expected by libheif.
In particular, this caused transparent backgrounds to be light gray due to the bottom end of the range starting at 16 instead of 0.
This PR also adds explicit handling of the mono alpha channel that's used to encode the transparency information in HEIC images when dealing with NV12 data.
P.S.: Not related to the changes in this PR, but I noticed that the alpha channel does not work properly when decode_with_browser_hevc returns RGBA data. I tried a few different ways to get it to work, but wasn't able to (the background is always black). I'm not sure if libheif is setup to handle the alpha channel mask when using RGBA data, or if there was an issue with my approach.
I was able to get it to work by converting RGBA data to NV12 and reusing that pathway. I know you didn't like that approach before, but it has a bit more meaning now to reuse the code b/c there's more custom code on each pathway for handling the mono/transparency mask. Let me know if you want a PR for that or if you want to try to work on getting alpha support to work with RGBA formatted data.
Note that from my perspective it is not that big of a problem, because I haven't yet seen any images with transparency that are using the code path that returns RGBA (I have to "force" it by modifying the code). But it might possibly be more likely on other hardware that I'm not testing on.
For my purposes, the webcodecs plugin is already very useful as it is, b/c HEIC with alpha channels are already somewhat rare to begin with.